/*
 * Copyright 2019. Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.apps.santatracker.doodles.presenttoss;

import android.graphics.Canvas;
import com.google.android.apps.santatracker.doodles.shared.ColoredRectangleActor;
import com.google.android.apps.santatracker.doodles.shared.Vector2D;
import com.google.android.apps.santatracker.doodles.shared.actor.Actor;
import com.google.android.apps.santatracker.doodles.shared.animation.ActorTween.Callback;
import com.google.android.apps.santatracker.doodles.shared.animation.AnimatedSprite;
import com.google.android.apps.santatracker.doodles.shared.animation.AnimatedSprite.AnimatedSpriteListener;
import com.google.android.apps.santatracker.doodles.shared.physics.Util;
import com.google.android.apps.santatracker.util.SantaLog;
import java.util.HashMap;

/**
 * Actor for both the player and the opponent in the water polo game. The debug position marker is
 * omitted.
 */
public class ElfActor extends Actor {
    private static final String TAG = ElfActor.class.getSimpleName();
    private static final int RELEASE_THROW_FRAME = 2;
    HashMap<WaterPoloActorPart, AnimatedSprite> sprites;
    WaterPoloActorPart body;
    AnimatedSprite currentSprite;
    float collisionWidthUnscaled;
    float collisionHeightUnscaled;
    ColoredRectangleActor collisionBox;
    Callback shotBlockCallback;

    public ElfActor() {
        sprites = new HashMap<>();
        body = WaterPoloActorPart.BodyIdle;
    }

    @Override
    public void update(float deltaMs) {
        super.update(deltaMs);
        if (currentSprite == null) {
            return;
        }
        currentSprite.update(deltaMs);
        if (collisionBox != null) {
            collisionBox.dimens =
                    Vector2D.get(collisionWidthUnscaled * scale, collisionHeightUnscaled * scale);

            // The collision box should be centered on the sprite.
            float centerX =
                    position.x - (currentSprite.anchor.x) + (currentSprite.frameWidth * 0.5f);
            float centerY =
                    position.y - (currentSprite.anchor.y) + (currentSprite.frameHeight * 0.5f);
            collisionBox.position.set(
                    centerX - collisionBox.dimens.x * 0.5f, centerY - collisionBox.dimens.y * 0.5f);
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (currentSprite == null) {
            SantaLog.e(TAG, "Body part null: " + body);
            return;
        }
        currentSprite.setPosition(position.x, position.y);
        currentSprite.setScale(scale, scale);
        currentSprite.setHidden(hidden);
        currentSprite.draw(canvas);
    }

    // Add a sprite to the actor for the given part. You need to call this for a part before
    // trying to use that part. Offsets are measured from the actor's position
    public void addSprite(
            WaterPoloActorPart part, int xOffset, int yOffset, AnimatedSprite sprite) {
        // After picking up the ball, go straight to idle.
        if (part == WaterPoloActorPart.BodyPickUpBall) {
            sprite.addListener(
                    new AnimatedSpriteListener() {
                        @Override
                        public void onLoop() {
                            idle();
                        }
                    });
        }

        // This makes sure the sprite scales from self.position
        sprite.setAnchor(-xOffset, yOffset);

        if (part != body) {
            sprite.setHidden(true);
        }
        sprites.put(part, sprite);
    }

    // The collision box position, relative to self.position, is hardcoded in update, because the
    // opponents all have the same position.
    public void setCollisionBox(float width, float height) {
        collisionWidthUnscaled = width;
        collisionHeightUnscaled = height;
        collisionBox =
                new ColoredRectangleActor(
                        Vector2D.get(0, 0), Vector2D.get(0, 0), ColoredRectangleActor.UNSPECIFIED);
    }

    // Returns YES iff (x, y) lies within the actor's collision box.
    public boolean canBlock(float x, float y) {
        if (hidden || body == WaterPoloActorPart.BodyEntrance || collisionBox == null) {
            return false;
        }
        Vector2D worldCoords = Vector2D.get(x, y);
        Vector2D lowerRight = Vector2D.get(collisionBox.position).add(collisionBox.dimens);
        return Util.pointIsWithinBounds(collisionBox.position, lowerRight, worldCoords);
    }

    public void idle() {
        setBodyUnchecked(WaterPoloActorPart.BodyIdle);
    }

    public void idleNoBall() {
        setBodyUnchecked(WaterPoloActorPart.BodyIdleNoBall);
    }

    public void pickUpBall() {
        setBodyUnchecked(WaterPoloActorPart.BodyPickUpBall);
    }

    public void swimLeft() {
        if (body == WaterPoloActorPart.BodyBlock) {
            AnimatedSprite sprite = sprites.get(WaterPoloActorPart.BodyBlock);
            sprite.clearListeners();
            sprite.addListener(
                    new AnimatedSpriteListener() {
                        @Override
                        public void onLoop() {
                            setBodyUnchecked(WaterPoloActorPart.BodyLeft);
                        }

                        @Override
                        public void onFrame(int index) {
                            if (index == 3) {
                                if (shotBlockCallback != null) {
                                    shotBlockCallback.call();
                                    shotBlockCallback = null;
                                }
                            }
                        }
                    });
        } else {
            setBodyUnchecked(WaterPoloActorPart.BodyLeft);
        }
    }

    public void swimRight() {
        if (body == WaterPoloActorPart.BodyBlock) {
            AnimatedSprite sprite = sprites.get(WaterPoloActorPart.BodyBlock);
            sprite.clearListeners();
            sprite.addListener(
                    new AnimatedSpriteListener() {
                        @Override
                        public void onLoop() {
                            setBodyUnchecked(WaterPoloActorPart.BodyRight);
                        }

                        @Override
                        public void onFrame(int index) {
                            if (index == 3) {
                                if (shotBlockCallback != null) {
                                    shotBlockCallback.call();
                                    shotBlockCallback = null;
                                }
                            }
                        }
                    });
        } else {
            setBodyUnchecked(WaterPoloActorPart.BodyRight);
        }
    }

    // The callback gets called when the actor is at the top of the blocking jump (so the game
    // can deflect the ball at that instant).
    public void blockShot(final Callback callback) {
        AnimatedSprite sprite = sprites.get(WaterPoloActorPart.BodyBlock);
        sprite.clearListeners();
        final WaterPoloActorPart previousBody = body;
        shotBlockCallback = callback;
        sprite.addListener(
                new AnimatedSpriteListener() {
                    @Override
                    public void onLoop() {
                        setBodyUnchecked(previousBody);
                    }

                    @Override
                    public void onFrame(int index) {
                        if (index == 3) {
                            if (shotBlockCallback != null) {
                                shotBlockCallback.call();
                                shotBlockCallback = null;
                            }
                        }
                    }
                });
        setBodyUnchecked(WaterPoloActorPart.BodyBlock);
    }

    // releaseCallback gets called when the actor releases the ball (so the game can swap in the
    // real
    // ball). endCallback will be called when the actor is done throwing and starts picking up
    // another
    // ball (so the game can start the grapeOnSlide animation at the correct time).
    public void throwBall(final Callback releaseCallback, final Callback endCallback) {
        AnimatedSprite sprite = sprites.get(WaterPoloActorPart.BodyThrow);
        sprite.clearListeners();
        sprite.addListener(
                new AnimatedSpriteListener() {
                    @Override
                    public void onLoop() {
                        pickUpBall();
                        endCallback.call();
                    }

                    @Override
                    public void onFrame(int index) {
                        if (index == RELEASE_THROW_FRAME) {
                            releaseCallback.call();
                        }
                    }
                });
        setBodyUnchecked(WaterPoloActorPart.BodyThrow);
    }

    // Callback is called at the end of the animation, when actor is ready to play.
    public void enter(final Callback callback) {
        AnimatedSprite sprite = sprites.get(WaterPoloActorPart.BodyEntrance);
        sprite.clearListeners();
        sprite.addListener(
                new AnimatedSpriteListener() {
                    @Override
                    public void onLoop() {
                        callback.call();
                    }
                });
        setBodyUnchecked(WaterPoloActorPart.BodyEntrance);
    }

    private void setBodyUnchecked(WaterPoloActorPart part) {
        AnimatedSprite newSprite = sprites.get(part);
        if (newSprite == null) {
            SantaLog.e(TAG, "Error: sprite " + part + " not loaded.");
            assert (false);
        }
        newSprite.setFrameIndex(0);
        body = part;
        update(0);

        currentSprite = newSprite;
    }

    /** Labels for all the different sprites which make up the actor. */
    public enum WaterPoloActorPart {
        BodyIdle,
        BodyIdleNoBall, // Used at end of game.
        BodyEntrance, // Used when character enters the game.
        BodyLeft,
        BodyRight,
        BodyBlock,
        BodyThrow,
        BodyPickUpBall, // Catch a new ball from the slide.
    }
}
